Домашнее задание №2

Визуальный анализ данных о сердечно-сосудистых заболеваниях

В задании предлагается с помощью визуального анализа ответить на несколько вопросов по данным о сердечно-сосудистых заболеваниях. Данные использовались в соревновании ML Boot Camp 5 (качать их не надо, они уже есть в репозитории).

Заполните код в клетках (где написано "Ваш код здесь") и ответьте на вопросы в веб-форме. Код отправлять никуда не нужно.

В соревновании предлагалось определить наличие/отсутствие сердечно-сосудистых заболеваний (ССЗ) по результатам осмотра пациента.

Описание данных.

Датасет сформирован из реальных клинических анализов, и в нём используются признаки, которые можно разбить на 3 группы:

Объективные признаки:

  • Возраст (age)
  • Рост (height)
  • Вес (weight)
  • Пол (gender)

Результаты измерения:

  • Артериальное давление верхнее и нижнее (ap_hi, ap_lo)
  • Холестерин (cholesterol)
  • Глюкоза (gluc)

Субъективные признаки (со слов пациентов):

  • Курение (smoke)
  • Употребление алкоголя (alco)
  • Физическая активность (active)

Целевой признак (который интересно будет прогнозировать):

  • Наличие сердечно-сосудистых заболеваний по результатам классического врачебного осмотра (cardio)

Возраст дан в днях. Значения показателей холестерина и глюкозы представлены одним из трех классов: норма, выше нормы, значительно выше нормы. Значения субъективных признаков — бинарны.

Все показатели даны на момент осмотра.


In [6]:
# подгружаем все нужные пакеты
import pandas as pd
import numpy as np

# игнорируем warnings
import warnings
warnings.filterwarnings("ignore")

import seaborn as sns

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker
%matplotlib inline

# настройка внешнего вида графиков в seaborn
sns.set_context(
    "notebook", 
    font_scale = 1.5,       
    rc = { 
        "figure.figsize" : (12, 9), 
        "axes.titlesize" : 18 
    }
)

В рамках задания для простоты будем работать только с обучающей выборкой. Чистить данные от выбросов и ошибок в данных НЕ нужно, кроме тех случаев, где об этом явно указано.

Все визуализации рекомендуем производить с помощью библиотеки Seaborn.

Проведем небольшой EDA


In [3]:
train = pd.read_csv('mlbootcamp5_train.csv', sep=';',
                    index_col='id')

In [4]:
print('Размер датасета: ', train.shape)
train.head()


Размер датасета:  (70000, 12)
Out[4]:
age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
id
0 18393 2 168 62.0 110 80 1 1 0 0 1 0
1 20228 1 156 85.0 140 90 3 1 0 0 1 1
2 18857 1 165 64.0 130 70 3 1 0 0 0 1
3 17623 2 169 82.0 150 100 1 1 0 0 1 1
4 17474 1 156 56.0 100 60 1 1 0 0 0 0

Для начала всегда неплохо бы посмотреть на значения, которые принимают переменные.

Переведем данные в "Long Format"-представление и отрисуем с помощью factorplot количество значений, которые принимают категориальные переменные.


In [4]:
train_uniques = pd.melt(frame=train, value_vars=['gender','cholesterol', 
                                                 'gluc', 'smoke', 'alco', 
                                                 'active', 'cardio'])
train_uniques = pd.DataFrame(train_uniques.groupby(['variable', 
                                                    'value'])['value'].count()) \
    .sort_index(level=[0, 1]) \
    .rename(columns={'value': 'count'}) \
    .reset_index()
    
sns.factorplot(x='variable', y='count', hue='value', 
               data=train_uniques, kind='bar', size=12);


Видим, что классы целевой переменной cardio сбалансированы, отлично!

Можно также разбить элементы обучающей выборки по значениям целевой переменной: иногда на таких графиках можно сразу увидеть самый значимый признак.


In [5]:
train_uniques = pd.melt(frame=train, value_vars=['gender','cholesterol', 
                                                 'gluc', 'smoke', 'alco', 
                                                 'active'], 
                        id_vars=['cardio'])
train_uniques = pd.DataFrame(train_uniques.groupby(['variable', 'value', 
                                                    'cardio'])['value'].count()) \
    .sort_index(level=[0, 1]) \
    .rename(columns={'value': 'count'}) \
    .reset_index()
    
sns.factorplot(x='variable', y='count', hue='value', 
               col='cardio', data=train_uniques, kind='bar', size=9);


Видим, что в зависимости от целевой переменной сильно меняется распределение холестерина и глюкозы. Совпадение?

Немного статистики по уникальным значениям признаков.


In [6]:
for c in train.columns:
    n = train[c].nunique()
    print(c)
    
    if n <= 3:
        print(n, sorted(train[c].value_counts().to_dict().items()))
    else:
        print(n)
    print(10 * '-')


age
8076
----------
gender
2 [(1, 45530), (2, 24470)]
----------
height
109
----------
weight
287
----------
ap_hi
153
----------
ap_lo
157
----------
cholesterol
3 [(1, 52385), (2, 9549), (3, 8066)]
----------
gluc
3 [(1, 59479), (2, 5190), (3, 5331)]
----------
smoke
2 [(0, 63831), (1, 6169)]
----------
alco
2 [(0, 66236), (1, 3764)]
----------
active
2 [(0, 13739), (1, 56261)]
----------
cardio
2 [(0, 35021), (1, 34979)]
----------

Итого:

  • Пять количественных признаков (без id)
  • Семь категориальных
  • 70000 объектов

1. Визуализируем корреляционную матрицу

Для того чтобы лучше понять признаки в датасете, можно посчитать матрицу коэффициентов корреляции между признаками.

Постройте heatmap корреляционной матрицы. Матрица формируется средствами Pandas, со стандартным значением параметров.

1. Какие два признака больше всего коррелируют (по Пирсону) с признаком height ?

  • Gluc, Cholesterol
  • Weight, Alco
  • Smoke, Alco
  • Weight, Gender

In [11]:
import seaborn

In [14]:
# Правильный ответ, больше всего коррелируют признаки Weight и Gender
df = pd.read_csv('mlbootcamp5_train.csv', sep=';')
corr = train.corr()
sns.heatmap(corr)
corr


Out[14]:
age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
age 1.000000 -0.022811 -0.081515 0.053684 0.020764 0.017647 0.154424 0.098703 -0.047633 -0.029723 -0.009927 0.238159
gender -0.022811 1.000000 0.499033 0.155406 0.006005 0.015254 -0.035821 -0.020491 0.338135 0.170966 0.005866 0.008109
height -0.081515 0.499033 1.000000 0.290968 0.005488 0.006150 -0.050226 -0.018595 0.187989 0.094419 -0.006570 -0.010821
weight 0.053684 0.155406 0.290968 1.000000 0.030702 0.043710 0.141768 0.106857 0.067780 0.067113 -0.016867 0.181660
ap_hi 0.020764 0.006005 0.005488 0.030702 1.000000 0.016086 0.023778 0.011841 -0.000922 0.001408 -0.000033 0.054475
ap_lo 0.017647 0.015254 0.006150 0.043710 0.016086 1.000000 0.024019 0.010806 0.005186 0.010601 0.004780 0.065719
cholesterol 0.154424 -0.035821 -0.050226 0.141768 0.023778 0.024019 1.000000 0.451578 0.010354 0.035760 0.009911 0.221147
gluc 0.098703 -0.020491 -0.018595 0.106857 0.011841 0.010806 0.451578 1.000000 -0.004756 0.011246 -0.006770 0.089307
smoke -0.047633 0.338135 0.187989 0.067780 -0.000922 0.005186 0.010354 -0.004756 1.000000 0.340094 0.025858 -0.015486
alco -0.029723 0.170966 0.094419 0.067113 0.001408 0.010601 0.035760 0.011246 0.340094 1.000000 0.025476 -0.007330
active -0.009927 0.005866 -0.006570 -0.016867 -0.000033 0.004780 0.009911 -0.006770 0.025858 0.025476 1.000000 -0.035653
cardio 0.238159 0.008109 -0.010821 0.181660 0.054475 0.065719 0.221147 0.089307 -0.015486 -0.007330 -0.035653 1.000000

2. Распределение роста для мужчин и женщин

Как мы увидели, в процессе исследования уникальных значений пол кодируется значениями 1 и 2, расшифровка изначально не была нам дана в описании данных, но мы догадались, кто есть кто, посчитав средние значения роста (или веса) при разных значениях признака gender. Теперь сделаем то же самое, но графически.

Постройте violinplot для роста и пола. Используйте:

  • hue – для разбивки по полу
  • scale – для оценки количества каждого из полов

Для корректной отрисовки, преобразуйте DataFrame в "Long Format"-представление с помощью функции melt в pandas.
еще один пример


In [17]:
longformat = pd.melt(frame=df, value_vars='height', id_vars='gender')
longformat.head()


Out[17]:
gender variable value
0 2 height 168
1 1 height 156
2 1 height 165
3 2 height 169
4 1 height 156

Постройте на одном графике два отдельных kdeplot роста, отдельно для мужчин и женщин. На нем разница будет более наглядной, но нельзя будет оценить количество мужчин/женщин.


In [19]:
sns.violinplot(data=longformat, x='variable', y='value', hue='gender', scale='count');
plt.show()



In [20]:
sns.kdeplot(df[df['gender'] == 1]['height'])
sns.kdeplot(df[df['gender'] == 2]['height'])
plt.show()



3. Ранговая корреляция

В большинстве случаев достаточно воспользоваться линейным коэффициентом корреляции Пирсона для выявления закономерностей в данных, но мы пойдём чуть дальше и используем ранговую корреляцию, которая поможет нам выявить пары, в которых меньший ранг из вариационного ряда одного признака всегда предшествует большему другого (или наоборот, в случае отрицательной корреляции).

Постройте корреляционную матрицу, используя коэффициент Спирмена

3.1 Какие признаки теперь больше всего коррелируют (по Спирмену) друг с другом?

  • Height, Weight
  • Age, Weight
  • Ap_hi, Ap_lo
  • Cholesterol, Gluc
  • Cardio, Cholesterol
  • Smoke, Alco

In [21]:
sns.heatmap(df.corr(method='spearman'));
plt.show()



In [26]:
df.corr(method='spearman')


Out[26]:
id age gender height weight ap_hi ap_lo cholesterol gluc smoke alco active cardio
id 1.000000 0.002682 0.003506 -0.001886 -0.001292 0.003258 -0.000238 0.005706 0.002789 -0.003697 0.001214 0.003766 0.003800
age 0.002682 1.000000 -0.020176 -0.082292 0.061559 0.219087 0.156700 0.140113 0.091791 -0.047471 -0.030078 -0.010422 0.234429
gender 0.003506 -0.020176 1.000000 0.533805 0.172480 0.063174 0.066374 -0.035730 -0.018866 0.338135 0.170966 0.005866 0.008109
height -0.001886 -0.082292 0.533805 1.000000 0.313569 0.019381 0.030534 -0.058940 -0.024369 0.197632 0.096670 -0.007367 -0.012383
weight -0.001292 0.061559 0.172480 0.313569 1.000000 0.277749 0.254420 0.137908 0.109901 0.071676 0.068088 -0.014894 0.182701
ap_hi 0.003258 0.219087 0.063174 0.019381 0.277749 1.000000 0.735436 0.208722 0.105334 0.028929 0.033243 -0.001268 0.451885
ap_lo -0.000238 0.156700 0.066374 0.030534 0.254420 0.735436 1.000000 0.167401 0.082474 0.026172 0.038862 0.003401 0.362572
cholesterol 0.005706 0.140113 -0.035730 -0.058940 0.137908 0.208722 0.167401 1.000000 0.407803 0.015220 0.041285 0.007573 0.215117
gluc 0.002789 0.091791 -0.018866 -0.024369 0.109901 0.105334 0.082474 0.407803 1.000000 0.000468 0.018584 -0.008610 0.091488
smoke -0.003697 -0.047471 0.338135 0.197632 0.071676 0.028929 0.026172 0.015220 0.000468 1.000000 0.340094 0.025858 -0.015486
alco 0.001214 -0.030078 0.170966 0.096670 0.068088 0.033243 0.038862 0.041285 0.018584 0.340094 1.000000 0.025476 -0.007330
active 0.003766 -0.010422 0.005866 -0.007367 -0.014894 -0.001268 0.003401 0.007573 -0.008610 0.025858 0.025476 1.000000 -0.035653
cardio 0.003800 0.234429 0.008109 -0.012383 0.182701 0.451885 0.362572 0.215117 0.091488 -0.015486 -0.007330 -0.035653 1.000000

3.2 Почему мы получили такое большое (относительно) значение ранговой корреляции у этих признаков?

  • Неточности в данных (ошибки при сборе данных)
  • Связь ошибочна, переменные никак не должны быть связаны друг с другом
  • Природа данных

4. Совместное распределение признаков

Постройте совместный график распределения jointplot двух наиболее коррелирующих между собой признаков (по Спирмену).

Кажется, наш график получился неинформативным из-за выбросов в значениях. Постройте тот же график, но с логарифмической шкалой (чтобы не получать OverflowError необходимо отфильтровать значения меньше либо равные нулю).


In [27]:
logged = train.loc[ (train['ap_hi'] > 0)&(train['ap_lo']>0), ['ap_hi', 'ap_lo'] ]
logged['ap_hi'] = np.log(logged['ap_hi'])
logged['ap_lo'] = np.log(logged['ap_lo'])

g = sns.jointplot(x = 'ap_hi', y = 'ap_lo', data = logged)
# -------------------- #

"""Сетка"""
g.ax_joint.grid(True) 

"""Преобразуем логарифмические значения на шкалах в реальные"""
g.ax_joint.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(round(int(np.exp(x))))))
g.ax_joint.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, pos: str(round(int(np.exp(x))))))


4.1 Сколько чётко выраженных кластеров получилось на совместном графике выбранных признаков, с логарифмической шкалой? Под кластером в данной задаче понимается плотное скопление точек, в окрестности которого пренебрежительно мало одиночных наблюдений и которое визуально отделимо от других кластеров.

  • 1
  • 2
  • 3
  • больше трёх

5. Возраст

Посчитаем, сколько полных лет было респондентам на момент их занесения в базу.


In [28]:
train['age_years'] = (train['age'] // 365.25).astype(int)

Постройте Countplot, где на оси абсцисс будет отмечен возраст, на оси ординат – количество. Каждое значение возраста должно иметь два столбца, соответствующих количеству человек каждого класса cardio (здоров/болен) данного возраста.

5. В каком возрасте количество пациентов с ССЗ впервые становится больше, чем здоровых?

  • 44
  • 49
  • 53
  • 62

In [39]:
sns.set_context(
    "notebook", 
    font_scale = 1,       
    rc = { 
        "figure.figsize" : (12, 9), 
        "axes.titlesize" : 18 
    }
)

In [40]:
train['age_years'] = (train['age'] // 365.25).astype(int)

sns.countplot(x='age_years', hue = 'cardio', data = train)


Out[40]:
<matplotlib.axes._subplots.AxesSubplot at 0x1ddafd14048>

In [41]:
a = pd.crosstab(train.age_years, train.cardio).reset_index()
a[a[0] < a[1]]


Out[41]:
cardio age_years 0 1
16 53 2061 2105
18 55 1890 2335
19 56 1541 1766
20 57 1703 2284
21 58 1401 1709
22 59 1468 2376
23 60 1243 1690
24 61 936 1997
25 62 657 1334
26 63 805 2132
27 64 574 1412

In [ ]: